from __future__ import division

import logging
logging.basicConfig(format="%(message)s", level=logging.DEBUG)

"""Conditions and restarts for python.

Overview
========
A condition scheme; can be used as an error handling mechanism, or more generally to coordinate the interactions between
high and low level code. Additionally, there is a restart mechanism, which allows low-level code to define a set of
continuations, and for the high-level code to select which one to use.

A condition is an object that can be signalled and then handled. In the context of error handling it can be viewed to be
similar to an exception, except the handler is executed in place, instead of unwinding the call stack to where the
handler was defined.

When a condition is signalled we search up the stack for an appropriate handler. The in place execution of handlers
causes a problem if a condition is re-signalled within a handler, because we will search back up the stack, possibly
finding the handler we are currently executing - we end up in an infinite loop. To resolve this problem, a mechanism
called the condition firewall is employed. When a condition is signalled and a handler is found to deal with it, the
position of that handler in the stack is set to be the condition firewall. Any searches further up the stack will then
start from that position.

The four basic mechanisms in the condition system are:
Condition.handler(function) - establish function as a handler
Condition.signal(*args, **kwds) - signal the condition with args and kwds
Condition.restart(name, function) - establish function as a restart named name
Condition.invoke(name, *args, **kwds) - invoke a restart name with args and kwds

We make heavy use of the python inspect module, using it to walk the stack and access the local and global scopes.

Establishing a handler
======================
A handler is simply a function, bound to a condition class. Calling Condition.handler(function) registers function as a
handler for Condition in the current stack frame. Condition.handler can be used as a decorator.

Establishing a restart
======================
A restart is also simply a function, but it is bound to a string. Calling Condition.restart(name, function) registers
function as a restart with the given name (string) in the current stack frame. An implementation feature is that
Condition.restart can be called without a function, and then returns a registration function, thus allowing it to be used
as a parameterised decorator.

Condition signalling sequence
=============================
0. Given a class Example extending Condition...
1. Call Example.signal(a, b, w=x, y=z)
2. An instance of Example is constructed, with positional arguments (a, b) and keyword arguments (w=x, y=z).
3. Loop:
4.     Search up the stack for a handler capable of dealing with Example (see note 1). If no handler can be found, raise
       an exception.
5.     Call the handler, passing in the condition instance.
6.     If the condition returns without raising a WillNotHandleCondition exception, return that value.

Restart invocation sequence
===========================
0. Given a class Example extending Condition...
1. Call Example.invoke('example restart', a, b, w=x, y=z)
2. Loop:
3.     Search up the stack for a restart called 'example restart' (see note 2). If no restart can be found, raise an
       exception.
4.     Call the restart, with positional arguments (a, b) and keyword arguments (w=x, y=z) and return its result.

Notes
=====
Note 1: There are two schemes for searching for handlers: prefer stack closeness, or prefer type closeness. Preferring
        stack closeness is the current standard, and means that we choose the first handler that we find. Preferring type
        closeness means we choose the handler whose defining class is as close to the front of Example's method
        resolution order as possible. Both schemes maintain the notion of the 'condition firewall'. This means that if a
        condition is re-signalled, we start from the next frame on from where we found the last handler.

Note 2: There are the same two schemes for searching for restarts as for handlers. With restarts however, the condition
        firewall does not apply.

Todo
====
* Make use of threading.local to store thread-local data.
* Make the 'magic' names definitely unique, by checking the namespaces before insertion.
* When searching preferring type closeness, perhaps make a list of handlers that might be suitable, so we only traverse
  the stack once? 
* It might be the case that if we have already signalled a condition of type T, then the signalling of that condition
  between the frame where it was signalled and the frame where the handler was previously found causes problems,
  specifically making use of the condition firewall state established from the previous condition.
* How hard is it to integrate conditions with more general message-passing?
* Ideally, we should search handler definitions in reverse order within stack frames, and also the condition firewall
  should maintain our position with the stack frame.
* Can restarts be modelled using conditions? - Probably not

Message.signal(*args, **kwds)
Message.handler(function(condition))

Message.restart(name, function(*args, **kwds))
Message.invoke(name, *args, **kwds)

Differences between conditions and restarts:
============================================
* Conditions have the condition firewall
* When a condition is signalled, a new condition instance is built, and that is then passed in to the handler
* When a restart is invoked, the arguments are passed straight to the restart

"""

import inspect
class Condition(object):

    handlers = object()
    restarts = object()
    firewall = object()

    class WillNotHandleCondition(Exception): pass 

    @classmethod
    def handler(cls, handler):
        """Register a new handler for this condition type."""  
        # Create a record of the handler, and the frame in which it was registered.
        # This allows us to make use of the same handler function in multiple places.
        frame = inspect.currentframe().f_back
        handler_record = (handler, frame)
        try:
            handlers = frame.f_locals[Condition.handlers]
            try:
                handlers[cls].insert(0, handler_record)
            except KeyError:
                handlers[cls] = [handler_record]
        except KeyError:
            frame.f_locals[Condition.handlers] = {cls:[handler_record]}
        logging.debug("Registered a handler for %s in %s stack frame."%(cls.__name__, frame.f_code.co_name))

    @classmethod
    def restart(cls, name, restart_function=None):
        """Return a function that can be used to register a restart with the given name."""
        def register(restart):
            """Register a new restart for this condition type."""
            if restart_function == None:
                frame = inspect.currentframe().f_back
            else:
                frame = inspect.currentframe().f_back.f_back
            # insert local restart
            try:
                restarts = frame.f_locals[Condition.restarts]
                try:
                    restarts[cls][name] = restart
                except KeyError:
                    restarts[cls] = {name:restart}
            except KeyError:
                frame.f_locals[Condition.restarts] = {cls:{name:restart}}
            # insert global restart
            try:
                restarts = frame.f_globals[Condition.restarts]
                try:
                    restarts[cls][name] = restart
                except KeyError:
                    restarts[cls] = {name:restart}
            except KeyError:
                frame.f_globals[Condition.restarts] = {cls:{name:restart}}
            logging.debug("Registered a restart %s for %s in %s stack frame."%(name, cls.__name__, frame.f_code.co_name))
        if restart_function == None:
            return register
        else:
            return register(restart_function)

    @classmethod
    def cerror(cls, *args, **kwds):
        cls.restart('continue')(lambda: None)
        return cls.signal(*args, **kwds)

    @classmethod
    def warn(cls, *args, **kwds):
        try:
            return cls.signal(*args, **kwds)
        # FIXME : make a special exception type for no handlers
        except Exception:
            logging.warn('%s %s %s'%(cls.__name__, args, kwds))
            return None

    @classmethod
    def signal(cls, *args, **kwds):
        """Signal a condition with the given arguments.
        
        Constructs an instance of the given condition class, and then calls _signal on it to do the real work.
        """
        condition = cls(*args, **kwds)
        logging.debug("Signalling a condition %s (%s)."%(cls.__name__, condition))
        return condition._signal_preferring_stack_closeness()
        
    def resignal(self):
        logging.debug("Resignalling a condition %s (%s)."%(self.__class__.__name__, self))
        return self._signal_preferring_stack_closeness()
        
    def invoke(self, name, *args, **kwds):
        """Invoke the named restart with the given arguments."""
        return self._invoke_preferring_stack_closeness(name, *args, **kwds)

    def _signal_preferring_stack_closeness(self):
        """Signal this condition.
        
        We have to search up the stack for handlers capable of dealing with us.
        """
        frame = inspect.currentframe().f_back
        try:
            frame = frame.f_globals[Condition.firewall]
        except KeyError:
            pass
        frame = frame.f_back
        while frame:
            try:
                handlers = frame.f_locals[Condition.handlers]
                logging.debug("Found some handlers in %s stack frame."%(frame.f_code.co_name))
                for cls in self.__class__.__mro__:
                    try:
                        cls_handlers = handlers[cls]
                        logging.debug("Found some handlers for %s in %s stack frame."%(cls.__name__, frame.f_code.co_name))
                        for handler, frame in cls_handlers:
                            try:
                                previous_firewall = frame.f_globals[Condition.firewall]
                            except KeyError:
                                previous_firewall = None
                            try:
                                logging.debug("Trying handler for %s defined in %s on line %d of %s."%(cls.__name__, frame.f_code.co_name, frame.f_code.co_firstlineno, frame.f_code.co_filename))
                                frame.f_globals[Condition.firewall] = frame
                                return handler(self)
                            except Condition.WillNotHandleCondition:
                                logging.debug("%s won't handle %s condition."%(handler, cls.__name__))
                            finally:
                                frame.f_globals[Condition.firewall] = previous_firewall
                    except KeyError:
                        logging.debug("Couldn't find any handlers for %s in %s stack frame."%(cls.__name__, frame.f_code.co_name))
            except KeyError:
                logging.debug("Couldn't find any handlers at all in %s stack frame."%(frame.f_code.co_name))
           
            # we haven't managed to find a handler willing to deal with us so move back a stack frame
            frame = frame.f_back
        else:
            # TODO : check for 'global' handlers
            # FIXME : make a special exception type for no handlers
            raise Exception('No way of handling %s.'%self)
 
    def _signal_preferring_type_closeness(self):
        """Signal this condition.
        
        We have to search up the stack for handlers capable of dealing with us.
        FIXME : This method doesn't use the condition firewall. Is this right?
        """
        frame = inspect.currentframe().f_back.f_back
        for cls in self.__class__.__mro__:
            while frame:  
                try:
                    handlers = frame.f_locals[Condition.handlers]
                    logging.debug("Found some handlers in %s stack frame."%(frame.f_code.co_name))
                    try:
                        cls_handlers = handlers[cls]
                        logging.debug("Found some handlers for %s in %s stack frame."%(cls.__name__, frame.f_code.co_name))
                        for handler in cls_handlers:
                            try:
                                logging.debug("Trying handler %s defined in %s on line %d of %s."%(handler, frame.f_code.co_name, frame.f_code.co_firstlineno, frame.f_code.co_filename))
                                return handler(self)
                            except Condition.WillNotHandleCondition:
                                logging.debug("%s won't handle %s condition."%(handler, cls.__name__))
                    except KeyError:
                        logging.debug("Couldn't find any handlers for %s in %s stack frame."%(cls.__name__, frame.f_code.co_name))
                except KeyError:
                    logging.debug("Couldn't find any handlers at all in %s stack frame."%(frame.f_code.co_name))
               
                # we haven't managed to find a handler willing to deal with us so move back a stack frame
                frame = frame.f_back
        else:
            # TODO : check for 'global' handlers
            # FIXME : make a special exception type for no handlers
            raise Exception('No way of handling %s.'%self)

    def _invoke_preferring_stack_closeness(self, name, *args, **kwds):
        """Invoke a restart with the given name and arguments."""
        
        logging.debug("Invoking a restart %s with arguments %s %s."%(name, args, kwds))

        frame = inspect.currentframe().f_back.f_back
        while frame:  
            try:
                restarts = frame.f_locals[Condition.restarts]
                logging.debug("Found some restarts in %s stack frame."%(frame.f_code.co_name))
                for cls in self.__class__.__mro__:
                    try:
                        restart = restarts[cls]
                        logging.debug("Found some restarts for %s in %s stack frame."%(cls.__name__, frame.f_code.co_name))
                        try:
                            logging.debug("Using restart %s(%s, %s) defined in %s on line %d of %s."%(name, args, kwds, frame.f_code.co_name, frame.f_code.co_firstlineno, frame.f_code.co_filename))
                            return restart[name](*args, **kwds)
                        except KeyError:
                            logging.debug("Couldn't find a restart named %s for %s in %s stack frame."%(name, cls.__name__, frame.f_code.co_name))
                    except KeyError:
                        logging.debug("Couldn't find any restarts for %s in %s stack frame."%(cls.__name__, frame.f_code.co_name))
            except KeyError:
                logging.debug("Couldn't find any restarts at all in %s stack frame."%(frame.f_code.co_name))
            # we haven't managed to find a restart with the correct name
            frame = frame.f_back
        else:
            # TODO : check for 'global' restarts
            frame = inspect.currentframe().f_back.f_back
            logging.info('globals %s'%(frame.f_globals,))
            
            raise Exception("Couldn't find a restart named %s."%name)

    def _invoke_preferring_type_closeness(self, name, *args, **kwds):
        """Invoke a restart with the given name and arguments."""
        
        logging.debug("Invoking a restart %s with arguments %s %s."%(name, args, kwds))
        
        frame = inspect.currentframe().f_back.f_back
        for cls in self.__class__.__mro__:
            while frame:  
                try:
                    restarts = frame.f_locals[Condition.restarts]
                    logging.debug("Found some restarts in %s stack frame."%(frame.f_code.co_name))
                    try:
                        restart = restarts[cls]
                        logging.debug("Found some restarts for %s in %s stack frame."%(cls.__name__, frame.f_code.co_name))
                        try:
                            logging.debug("Using restart %s(%s, %s) defined in %s on line %d of %s."%(name, args, kwds, frame.f_code.co_name, frame.f_code.co_firstlineno, frame.f_code.co_filename))
                            return restart[name](*args, **kwds)
                        except KeyError:
                            logging.debug("Couldn't find a restart named %s for %s in %s stack frame."%(name, cls.__name__, frame.f_code.co_name))
                    except KeyError:
                        logging.debug("Couldn't find any restarts for %s in %s stack frame."%(cls.__name__, frame.f_code.co_name))
                except KeyError:
                    logging.debug("Couldn't find any restarts at all in %s stack frame."%(frame.f_code.co_name))
                # we haven't managed to find a restart with the correct name
                frame = frame.f_back
            else:
                # TODO : check for 'global' restarts
                frame = inspect.currentframe().f_back.f_back
                logging.info('globals %s'%(frame.f_globals,))
            
                raise Exception("Couldn't find a restart named %s."%name)


if __name__ == '__main__':
    
    class DivideByZeroCondition(Condition):
        def __init__(self, n):
            self.n = n
            
        def __str__(self):
            return '%f/0'%self.n
    
    @Condition.restart('abort')
    def abort(restart):
        raise Exception('Aborted')
    
#    @Condition.handler
#    def handler(condition):
#        print 'Outer handler for %s'%condition
#        return condition.invoke('return 0')

    
    def low_level(a, b):
        if b == 0:
            
            DivideByZeroCondition.restart('return 0', lambda: 0)         
            DivideByZeroCondition.restart('return n', lambda n: n)
            
            @DivideByZeroCondition.restart('retry n')
            def retry(n):
                return low_level(a, n)
            
#            DivideByZeroCondition.warn(a)
            
            return DivideByZeroCondition.signal(a)
        else:
            return a / b
        
    def mid_level(divisor):
        @DivideByZeroCondition.handler
        def handler(condition):
            logging.info('Mid level handler for %s'%condition)
            logging.warn("I don't know how to deal with this!")
            return condition.resignal()
        return low_level(3, divisor)
    
    def high_level():
        @DivideByZeroCondition.handler
        def handler(condition):
            logging.info('High level handler for %s'%condition)
            return condition.invoke('return n', 1e300)
        
        for i in range(-10, 10):
            logging.info('calling mid_level with %d'%i)
            yield 3, i, mid_level(i)
    
    list = [i for i in high_level()]
    
    for i in list:
        print ("%d/%d = %f"%i)
